基本Canvas绘图
Canvas起步
<canvas>
就是一块画布,就是你提笔挥洒写意的地方。从标记的角色看,它简单明了,只要给它指定三个属性即可:id
、width
和height
。
html
<canvas id="myCanvas" width="500" height="300"></canvas>
其中,width
和height
指定的就是这块“画布”的宽度和高度,单位是像素。
注意:一定要通过width和height属性设置宽高,而不要在样式表中设置。
开始的时候,canvas
在页面上会显示一块空白、无边框的矩形。为了让它在页面上显示轮廓,可以通过一条样式规则为它应用不同的背景颜色或边框:
css
#myCanvas {
border: 1px dashed #000;
}
开始绘图之前,需要JavaScript执行两步操作。
js
// 第一步:拿到canvas元素
var canvas = document.getElementByID('myCanvas');
// 第二步:取得二维绘图上下文:
var context = canvas.getContext('2d');
什么上绘图上下文?你可以把它想象成一个超级强大的绘图工具,它可以帮你完成所有绘图任务,比如绘制矩形、输出文本、嵌入图片……总之,所有绘图操作都是通过它来完成的。
取得上下文对象之后,任何时候都可以进行绘图了。下面是一个模板例子:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#myCanvas {
border: 1px dashed #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="300"></canvas>
<script>
window.onload = function () {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// (把你自己的绘图代码写在这下面)
}
</script>
</body>
</html>
画直线
在我们涂鸦之前,还得先了解一个基本知识点:画布的坐标系。与其他HTML元素一样,<canvas>
坐标的左上角是坐标原点(0,0)。向右移动,x值增大;向下移动,y值增大。
最简单的绘图操作就是画一条实心直线。为此,需要通过绘图上下文执行三个操作:
- 使用
moveTo()方法
找到直线的起点。 - 使用
lineTo()方法
在起点和终点之间建立联系。 - 调用
stroke()方法
把直线绘制出来。
js
// 把起点定位到坐标(10,10)上
ctx.moveTo(10, 10);
// 建立起点(10,10)与终点(400,40)的联系
ctx.lineTo(400, 40);
// 绘制直线
ctx.stroke();
在调用stroke()方法
之前,你可以在任何时候设置绘图上下文的3个属性:lineWidth(线条宽度)
、strokeStyle(线条的颜色)
和lineCap(线条两端点的形状)
。这几个属性会一直影响后面的绘图操作,除非你再次修改它们的值。
js
// 设置线条的宽度,单位为像素
ctx.lineWidth = 10;
// 设置线条的颜色
ctx.strokeStyle = '#cd2828';
// 或者
ctx.strokeStyle = 'rgb(205, 40, 40)';
// 注意:无论使用哪种颜色表示法,都必须把值放到一对引号里面。
// 设置端点的形状,即线头类型
// 默认值是butt,即方头
// round,圆头
// square,加长方头
// 注意:圆头和加长方头会在线条的两头各增加一半的线宽长度。
ctx.lineCap = 'butt';
ctx.lineCap = 'round';
ctx.lineCap = 'square';
在下面的例子中,很清楚可以看到3种不同lineCap
的区别:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>canvas demo</title>
<style>
#myCanvas {
border: 1px dashed #ddd;
margin: 20px auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
window.onload = function () {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// 设置线条宽度
ctx.lineWidth = 50;
// 设置线条的颜色
ctx.strokeStyle = '#f00';
// 先使用默认的butt样式画一条从(50,50)到(400,50)的红色直线
ctx.moveTo(50, 50);
ctx.lineTo(400, 50);
ctx.stroke();
// 然后使用round样式画一条从(50, 150)到(400,150)的绿色直线
ctx.beginPath();
ctx.strokeStyle = '#0f0'
ctx.lineCap = 'round';
ctx.moveTo(50, 150);
ctx.lineTo(400, 150);
ctx.stroke();
// 最后使用square样式画一条从(50, 250)到(400,250)的蓝色直线
ctx.beginPath();
ctx.strokeStyle = '#00f';
ctx.lineCap = 'square';
ctx.moveTo(50, 250);
ctx.lineTo(400, 250);
ctx.stroke();
// 标记一下实际的终点和起点位置
ctx.beginPath();
ctx.moveTo(400, 50);
ctx.lineTo(400, 250);
ctx.strokeStyle = '#000';
ctx.lineCap = 'butt';
ctx.lineWidth = 1;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(50, 250);
ctx.stroke();
}
</script>
</body>
</html>
如下图所示:
在上面的例子中,我们可以看到每次重新开始一段新的绘制,绘图上下文都会调用一次beginPath()方法
,如果没有这一步,那么每次调用stroke()
都会把画布上原有的线段再重新绘制一遍。在修改了其他上下文属性的情况下,这个问题会比较明显。以上面的代码为例,如果不调用beginPath()
的话,那么就会发生在原有直线上以新颜色、新宽度或新线头形状重新绘制的问题。
注意:尽管开始绘制新线段的时候要调用beginPath()
,但结束绘制线段则不一定要做什么。每次开始新路径时,原来的路径就会自动“完成”。
路径与形状
为了确保三条直线各自独立,上一个例子将每条直线都按照新路径来绘制。这样可以为不同的直线分别应用不同的样式。实际上,路径本身也是很有用的,因为可以通过路径来填充自定义的形状。
js
ctx.moveTo(250, 50);
ctx.lineTo(50, 250);
ctx.lineTo(450, 250);
ctx.lineTo(250, 50);
ctx.lineWidth = 10;
ctx.strokeStyle = 'red';
ctx.stroke();
上面的代码是绘制一个红色边框的空心三角形。如果想给这个三角形填充颜色,那么stroke()
是无能为力的。此时,应该先调用closePath()
来明确地关闭路径,然后再把fillStyle属性
设置为想要的填充颜色,最后再调用fill()
来完成填充操作:
js
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill();
这个例子还有两个地方有必要调整一下:首先,如果,知道最后会关闭路径,那么实际上就不必再绘制最后一条线段,因为closePath()
会自动为最后一个绘制点与绘制起点 间绘制一条线,形成路径闭合状态;其次,最好是先填充开关,然后再绘制其轮廓,否则,开关的轮廓会有一部分被填充色覆盖掉。所以完整的绘制三角形的代码如下:
js
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.moveTo(250, 50);
ctx.lineTo(50, 250);
ctx.lineTo(450, 250);
ctx.closePath();
// 填充内部
ctx.fillStyle = 'blue';
ctx.fill();
// 绘制轮廓
ctx.lineWidth = 10;
ctx.strokeStyle = 'red';
ctx.stroke();
多数情况下,如果想要绘制复杂的开关,你都需要自己逐个线段地绘制。但有一个例外,那就是绘制矩形。绘制矩形可以使用fillRect()方法
直接填充一个矩形区域,只需要为它提供矩形区域左上角的坐标、宽度 和高度 即可。
js
// 在坐标(0, 10)上放置一个 100px * 200px 的矩形
ctx.fillRect(0, 10, 100, 200);
与fill()
一样,fillRect()
也是从绘图上下文的fillStyle属性
取得颜色。
类似的,还有一个strokeRect()方法
,可以直接绘制矩形框 :
js
ctx.strokeRect(0, 10, 100, 200);
绘制矩形框时,strokeRect()
的宽度取自lineWidth属性
,而边框宽度和颜色则取自strokeStyle属性
,与stroke()
一样。
绘制曲线
绘制曲线的4个方法:arc()
、arcTo
、bezierCurveTo()
和quadraticCurveTo()
。使用这几个方法分别能以不同的方式绘制曲线。
这4个方法里面,arc()
是最简单的,它可以绘制一段圆弧。arc()
接受5个参数:圆心的x坐标
、圆心的y坐标
、半径
、起点的角度(用弧度表示,即常量PI的倍数,1PI为半圆,2PI为整个圆形)
和终点的角度
。看下图:
js
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// 圆弧各方面的信息
var centerX = 150;
var centerY = 300;
var radius = 100;
var startingAngle = 1.25 * Math.PI;
var endingAngle = 1.75 * Math.PI;
ctx.arc(centerX, centerY, radius, startingAngle, endingAngle);
ctx.stroke();
如果在调用stroke()
之前调用closePath()
,就会在圆弧的起点和终点之间绘制一条直线。于是,就可以得到一个封闭的小半圆。
实际上,圆形也就是这么个圆弧继续向两端伸展构成的。因此如果想画一个整圆,可以这样:
js
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// 圆弧各方面的信息
var centerX = 150;
var centerY = 300;
var radius = 100;
var startingAngle = 0;
var endingAngle = 2 * Math.PI;
ctx.arc(centerX, centerY, radius, startingAngle, endingAngle);
ctx.stroke();
接下来要介绍的三个方法(arcTo()
、bezierCurveTo()
和quadraticCurveTo()
)要用到同一个概念:控制点。控制点本身并不包含在最终的曲线里,但能够影响曲线的最终形状。最好的例子就是贝塞尔曲线。下图展示了贝塞尔曲线 的控制点。
js
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// 创建变量
var startPoint = [62, 242];
var control1 = [187, 32];
var control2 = [429, 480];
var endPoint = [365, 133];
// 移动到起点位置
ctx.moveTo(startPoint[0], startPoint[1]);
// 使用bezierCurveTo()绘制曲线
// 参数分别是:
// 第一个控制点的X坐标
// 第一个控制点的Y坐标
// 第二个控制点的X坐标
// 第二个控制点的Y坐标
// 终点的X坐标
// 终点的Y坐标
ctx.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], endPoint[0], endPoint[1]);
ctx.stroke();
复杂而自然的形式通常需要多个圆弧和曲线拼接而成。完成之后,可以调用closePath()
以便填充,或者显示出完成的轮廓。
变换
变换,就是一种通过变化<canvas>坐标系
达到绘制目的的技术。例如,你想在三个地方绘制相同的正方形,为此,可以调用三次rect()
,每次都传入不同的起点位置:
js
ctx.rect(0, 0, 30, 30);
ctx.rect(50, 50, 30, 30);
ctx.rect(100, 100, 30, 30);
或者,也可以在同一个地方调用三次rect()
,但每次都移动一下坐标系,最终也能达到要求。比如:
js
// 在(0, 0)点绘制正方形
ctx.rect(0, 0, 30, 30);
// 把坐标系向下,向右各移动50像素
ctx.translate(50, 50);
ctx.rect(0, 0, 30, 30);
// 把坐标系再向下向右移一点
// 注意:变换是可以累积的
// 因此现在(0, 0)点实际被平移到了(100, 100)
ctx.translate(50, 50);
ctx.rect(0, 0, 30, 30);
ctx.stroke();
表面上看,变换不过就是把一些复杂的绘图任务变得更复杂了。但在处理一些棘手的问题的场合,使用变换却能收到神奇的效果。例如,你有一个函数,负责绘制一系列复杂的图形,最终再将它们组合成一幅鸟的图片。现在,你准备让鸟动起来,在<canvas>
里飞翔。
如果没有变换,要实现这个目标必须在每次绘制鸟的时候调整一次坐标。而有了变换,绘图代码可以不变,只要反复修改坐标系的位置就好了。
使用变换有几种不同的方式:
- 平移(
translate
):移动了坐标系的原点,默认位置是在canvas
的左上角。 - 缩放(
scale
):把原本要绘制的形状放大或缩小。 - 旋转(
rotate
):旋转坐标系。 - 矩阵(
matrix
):在任意方向拉伸和扭曲坐标系,必须要理解复杂的矩阵计算,才能实现自己想要的效果。
注意:变换是累积的。
js
// 移动(0, 0)点,这一步很重要
// 因为接下来要围绕新的原点旋转
ctx.translate(100, 100);
// 绘制10个正文形
var copies = 10;
for (var i = 1; i < copies; i++) {
// 绘制正方形之前,先旋转坐标系
// 旋转一周是2*Math.PI,因此每个正方形旋转的角度取决于要绘制的总数
ctx.rotate(2 * Math.PI * 1 / (copies - 1));
// 绘制正方形
ctx.rect(0, 0, 60, 60);
}
ctx.stroke();
Tips:调用绘图上下文的save()
可以保存坐标系当前的状态。然后,再调用restore()
可以返回保存过的前一个状态。如果要保存坐标系的状态,必须在应用任何变换之前调用save()
,这样再调用restore()
才能把坐标系恢复到正常状态。而在多步操作绘制复杂图形时,往往都需要多次保存坐标系状态。这些状态就如同浏览器的历史记录一样。
透明度
canvas
支持使用半透明的颜色,从而实现多外形状叠加透视的效果。有两种创建透明图形的方式:
- 使用
rgba()
设置透明颜色,即设置fillStyle或strokeStyle
。
js
// 设置填充及描边颜色
ctx.lineWidth = 10;
ctx.fillStyle = 'rgb(100, 150, 185)';
ctx.strokeStyle = 'red';
// 绘制圆形
ctx.arc(110, 120, 100, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
// 绘制三角形
ctx.beginPath();
// 用半透明的颜色填充三角形,描边色不变
ctx.fillStyle = 'rgba(100, 150, 185, .5)';
ctx.moveTo(215, 50);
ctx.lineTo(15, 250);
ctx.lineTo(315, 250);
ctx.closePath();
ctx.fill();
ctx.stroke();
- 使用绘制上下文的
globalAlpha属性
。
js
ctx.globalAlpha = .5;
// 此时,再设置的颜色不透明度都将是0.5;
ctx.fillStyle = 'rgb(100, 150, 185)';
合成操作
合成操作,就是告诉canvas
怎么显示两个重叠的图形。默认的合成操作是source-over
,即新绘制的图形会位于先绘制的图片之上(即覆盖第一个图形),如果新图形和第一个图形重叠,新的会遮盖住第一个。
在合成操作中,源(source)
指的是正在绘制的图片,目标(destination)
指的是画布上已经绘制的内容。
合成操作有很多种方式:
source-over
:默认。在目标图像上显示源图像。source-in
:在目标图像顶部显示源图像。源图像位于目标图像之外的部分是不可见的。source-out
:在目标图像中显示源图像。只有目标图像内的源图像部分会显示,目标图像是透明的。source-atop
:在目标图像之外显示源图像。只会显示目标图像之外源图像部分,目标图像是透明的。destination-over
:在源图像上方显示目标图像。destination-in
:在源图像顶部显示目标图像。源图像之外的目标图像部分不会被显示。destination-out
:在源图像中显示目标图像。只有源图像内的目标图像部分会被显示,源图像是透明的。destination-atop
:在源图像外显示目标图像。只有源图像外的目标图像部分会被显示,源图像是透明的。lighter
:显示源图像 + 目标图像。copy
:显示源图像。忽略目标图像。xor
:使用异或操作对源图像与目标图像进行组合。darker
:这种操作在不同的浏览器下表现不一样,不作探讨。
各种方式的不区别可以看下图:
要改变canvas
当前使用的合成操作方式,只要在画后面的图形之前设置绘图上下文的globalCompositeOperation属性
即可。
html
TYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>canvas-globalCompositeOperation</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
border: 1px dashed #ddd;
margin: 20px;
}
#container {
width: 700px;
margin: 20px auto;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
window.onload = function () {
function draw (name) {
var canvas = document.createElement('canvas');
canvas.width = 130;
canvas.height = 150;
document.getElementById('container').appendChild(canvas);
var ctx = canvas.getContext('2d');
// 绘制矩形
ctx.fillStyle = 'red';
ctx.fillRect(20, 20, 60, 60);
// 选择合成模式
ctx.globalCompositeOperation = name;
// 绘制圆形
ctx.beginPath();
ctx.fillStyle = 'blue';
ctx.arc(80, 80, 30, 0, 2 * Math.PI);
ctx.fill();
// 写上文字
// 这里需要重置一下合成模式,不然会出现意想不到的画面。
ctx.globalCompositeOperation = 'source-over'
ctx.beginPath();
ctx.fillStyle = '#000';
ctx.font = '12px Georgia';
ctx.fillText(name, 20, 130);
}
[
'source-over',
'source-in',
'source-out',
'source-atop',
'destination-over',
'destination-in',
'destination-out',
'destination-atop',
'lighter',
'copy',
'xor'
].map(function (name) {
draw(name);
})
}
</script>
</body>
</html>