基本Canvas绘图

前端开发
2018年12月15日
750

Canvas起步

<canvas>就是一块画布,就是你提笔挥洒写意的地方。从标记的角色看,它简单明了,只要给它指定三个属性即可:idwidthheight

html
<canvas id="myCanvas" width="500" height="300"></canvas>

其中,widthheight指定的就是这块“画布”的宽度和高度,单位是像素。

注意:一定要通过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值增大

最简单的绘图操作就是画一条实心直线。为此,需要通过绘图上下文执行三个操作:

  1. 使用moveTo()方法找到直线的起点。
  2. 使用lineTo()方法在起点和终点之间建立联系。
  3. 调用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>

如下图所示:

canvas线头

在上面的例子中,我们可以看到每次重新开始一段新的绘制,绘图上下文都会调用一次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()arcTobezierCurveTo()quadraticCurveTo()。使用这几个方法分别能以不同的方式绘制曲线。

这4个方法里面,arc()是最简单的,它可以绘制一段圆弧。arc()接受5个参数:圆心的x坐标圆心的y坐标半径起点的角度(用弧度表示,即常量PI的倍数,1PI为半圆,2PI为整个圆形)终点的角度。看下图:

canvas-arc

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())要用到同一个概念:控制点。控制点本身并不包含在最终的曲线里,但能够影响曲线的最终形状。最好的例子就是贝塞尔曲线。下图展示了贝塞尔曲线 的控制点。

canvas-贝赛尔曲线

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支持使用半透明的颜色,从而实现多外形状叠加透视的效果。有两种创建透明图形的方式:

  1. 使用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();
  1. 使用绘制上下文的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>