这篇学习和回顾canvas系列笔记的第二篇,完整笔记详见:canvas核心技术javascript
经过上一篇canvas核心技术-如何绘制线段的学习,咱们知道了如何去绘制线段。不少的线段的拼接就组成了图形了,好比常见的三角形,矩形,圆形等。java
常见图形的绘制能够查看个人在线示例:canvas shapegit
示例项目仓库地址:canvas demogithub
先来看看如何绘制一个三角形。三角形就是由三条边组成,咱们能够理解为三个线段组成。肯定了三角形的三个顶点的坐标位置,而后用线链接起来。canvas
let point1 = [100, 30]; //顶底1
let point2 = [50, 100]; //顶点2
let point3 = [180, 120]; //顶点3
ctx.beginPath(); //开始一段新路径
ctx.moveTo(point1[0], point1[1]); //移动起点到顶点1
ctx.lineTo(point2[0], point2[1]); //链接顶点1与顶点2
ctx.lineTo(point3[0], point3[1]); //链接顶点2与顶点3
ctx.stroke(); //描边
//绘制顶点坐标显示出来
ctx.textAlign='center'; //绘制文本水平居中
ctx.fillText(`(${point1[0]},${point1[1]})`, point1[0], point1[1]-10); //绘制顶点1文本
ctx.fillText(`(${point2[0]},${point2[1]})`, point2[0]-25, point2[1]+5); //绘制顶点2文本
ctx.fillText(`(${point3[0]},${point3[1]})`, point3[0]+30, point3[1]+5); //绘制顶点3文本
复制代码
从图能够看到,咱们还有一条边没有链接起来,这是由于咱们只显示的链接了2个顶点。要想把第三条边也链接起来,咱们有2种方式。第一种方式是,咱们显示的链接顶点3与顶点1浏览器
//第一种方式,显示的链接顶点3于顶点1
ctx.lineTo(point1[0], point1[1]);
复制代码
第二种方式是,咱们调用ctx.closePath()
来按canvas自动帮咱们链接未关闭的路径。bash
//第二种方式,调用ctx.closePath()
ctx.closePath();
复制代码
不管哪种均可以实现咱们想要三角形。其中第二种方式会用的比较多,由于它会帮咱们自动关闭当前路径,也就是使当前路径造成一个闭合的路径,这个在填充时是很是有用的,下面会说的。最终,咱们获得三角形图形以下函数
经过三个顶点,咱们能够绘制一个三角形,那么经过四个点,咱们固然能够绘制出四边形,咱们照例来经过四个点来绘制一个矩形。post
let point1 = [80, 30]; //p1
let point2 = [180, 30]; //p2
let point3 = [80, 110]; //p3
let point4 = [180, 110]; //p4
ctx.strokeStyle = 'green'; //设置描边颜色为绿色
ctx.beginPath(); //开始新的一段路径
ctx.moveTo(point1[0], point1[1]); //移动起点到p1
ctx.lineTo(point2[0], point2[1]); //链接p1与p2
ctx.lineTo(point4[0], point4[1]); //链接p2与p4
ctx.lineTo(point3[0], point3[1]); //链接p4与p3
ctx.closePath(); //关闭当前路径,隐士链接p3与p1
ctx.stroke(); //描边
//绘制顶点
ctx.textAlign = 'center';
ctx.fillText('p1', point1[0] - 10, point1[1] - 10);
ctx.fillText('p2', point2[0] + 10, point2[1] - 10);
ctx.fillText('p3', point3[0] - 10, point3[1] + 10);
ctx.fillText('p4', point4[0] + 10, point4[1] + 10);
复制代码
注意,咱们的顺序是p1-->p2-->p4--P3,因为矩形是一种特殊的四边形,在canvas中提供了一种方法能够快速建立一个矩形,若是知道了p1的坐标和矩形的宽度和高度,那么咱们就能够肯定了其余三个点的坐标。学习
//快速建立矩形
ctx.rect(point1[0], point1[1], 100, 80);
复制代码
在建立矩形,咱们老是使用ctx.rect(left,top,width,height)
,可是绘制非矩形的四边形,仍是得按照每一个点去链接成线段来绘制。
圆形能够看做是无数个很小的线段链接起来的,可是经过去定顶点来绘制圆形,显然不现实。canvas中提供了一个专门绘制圆形的方法ctx.arc(left,top,radius,startAngle,endAngle,antiClockwise)
。各个参数的顺序意思是,圆心坐标X值,圆心坐标Y值,半径,开始弧度,结束弧度,是否逆时针。经过指定startAngle=0
和endAngle=Math.PI*2
,就能够绘制一个完整的圆了。最后一个参数antiClockwise
对于图片的填充时会很是有用,后面讲填充时会详细说到。
let center = [100, 75]; //圆心坐标
let radius = 50; //半径
let startAngle = 0; //开始弧度值
let endAngle = Math.PI * 2; //结束弧度值,360度=Math.PI * 2
let antiClockwise = false; //是否逆时针
ctx.strokeStyle = 'blue'; //描边颜色
ctx.lineWidth = 1;
ctx.arc(center[0], center[1], radius, startAngle, endAngle, antiClockwise);
ctx.stroke(); //将圆形描边绘制出来
//绘制出圆心和半径示意图,读者能够忽略下半部代码
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(center[0], center[1], 2, startAngle, endAngle, antiClockwise);
ctx.fill();
ctx.beginPath();
ctx.moveTo(center[0], center[1]);
ctx.lineTo(center[0] + radius, center[1]);
ctx.stroke();
ctx.fillStyle = 'blue';
ctx.font = '24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('r', center[0] + radius / 2, center[1] - 10);
复制代码
let center = [50, 75]; //圆心坐标
let radius = 20; //半径
let startAngle = 0; //起始弧度为0
let antiClockwise = false; //是否逆时针
let angles = [1 / 8, 1 / 4, 1 / 2, 3 / 4]; //弧度长度
let colors = ['red', 'blue', 'green', 'orange']; //描边颜色
for (let [i, angle] of angles.entries()) {
let endAngle = Math.PI * 2 * angle; //计算结束弧度
ctx.strokeStyle = colors[i]; //设置描边颜色
ctx.beginPath(); //开始新的路径
ctx.arc(center[0] + i * radius * 3, center[1], radius, startAngle, endAngle, antiClockwise); //绘制圆弧
ctx.stroke(); //描边
}
复制代码
上面说的都是一些比较简单和常见的图形,咱们如何能够绘制任意多边形,好比五边形,六边形,八边形等。其实,在绘制四边形的时候就说过了,能够经过肯定顶点坐标,而后把这些顶点按照必定顺序链接起来就能够了。下面,来实现一个通用的多边形的绘制方法。
class Polygon {
constructor(ctx, points) {
this.ctx = ctx;
this.points = points;
}
draw() {
if (!this.ctx instanceof CanvasRenderingContext2D) {
throw new Error('Polygon#ctx must be an CanvasRenderingContext2D instance');
}
if (!Array.isArray(this.points)) {
throw new Error('Polygon#points must be an Array');
}
if (!this.points.length) {
return;
}
let firstPoint = this.points[0];
let restPoint = this.points.slice(1);
ctx.beginPath();
ctx.moveTo(firstPoint[0], firstPoint[1]);
for (let point of restPoint) {
ctx.lineTo(point[0], point[1]);
}
ctx.closePath();
}
}
复制代码
经过实例化这个Polygon
,并传入多边形的顶点坐标,咱们就能够绘制出不一样的多边形。例以下面的代码,分别绘制了五边形,六边形。
//绘制五边形
let points = [[30, 40], [80, 40], [100, 80], [55, 120], [10, 80]];
let pentagon = new Polygon(ctx, points);
ctx.strokeStyle = 'blue';
pentagon.draw();
ctx.stroke();
//绘制六边形
points = [[160, 40], [210, 40], [230, 80], [210, 120], [160, 120], [140, 80]];
let hexagon = new Polygon(ctx, points);
ctx.strokeStyle = 'green';
hexagon.draw();
ctx.stroke();
复制代码
上面,咱们都是用描边把图形绘制出来,还有一种用的比较多的就是填充了。填充就是用特定的颜色把图形包围的区域涂满。
let point1 = [100, 30]; //顶底1
let point2 = [50, 100]; //顶点2
let point3 = [180, 120]; //顶点3
ctx.strokeStyle = 'red'; //用红色描边
ctx.fillStyle = 'yellow'; //用黄色填充
ctx.lineWidth = 2; //设置线段宽度为2
ctx.beginPath(); //开始一段新路径
ctx.moveTo(point1[0], point1[1]); //移动起点到顶点1
ctx.lineTo(point2[0], point2[1]); //链接顶点1与顶点2
ctx.lineTo(point3[0], point3[1]); //链接顶点2与顶点3
ctx.closePath(); //关闭当前路径
ctx.stroke(); //描边
ctx.fill(); //填充
复制代码
须要注意的是,若是当前路径没有关闭,那么会先默认关闭当前路径,而后在进行填充 ,以下,咱们把ctx.closePath()
注释掉。
let point1 = [100, 30]; //顶底1
let point2 = [50, 100]; //顶点2
let point3 = [180, 120]; //顶点3
ctx.strokeStyle = 'red'; //用红色描边
ctx.fillStyle = 'yellow'; //用黄色填充
ctx.lineWidth = 2; //设置线段宽度为2
ctx.beginPath(); //开始一段新路径
ctx.moveTo(point1[0], point1[1]); //移动起点到顶点1
ctx.lineTo(point2[0], point2[1]); //链接顶点1与顶点2
ctx.lineTo(point3[0], point3[1]); //链接顶点2与顶点3
// ctx.closePath(); //关闭当前路径
ctx.stroke(); //描边
ctx.fill(); //填充
复制代码
若是当前路径是循环的,或者是包含多个相交的子路径,那么canvas何如进行填充呢?好比下面这样的,为什么在填充时,中间这一块没有被填充?
let point1 = [100, 30];
let point2 = [50, 100];
let point3 = [180, 120];
let point4 = [50, 60];
let point5 = [160, 80];
let point6 = [70, 120];
ctx.strokeStyle = 'red';
ctx.fillStyle = 'yellow';
ctx.lineWidth = 2;
ctx.beginPath(); //开始一段新路径
//绘制三角形1, 顺序:p1--p2--p3--p1
ctx.moveTo(point1[0], point1[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.lineTo(point3[0], point3[1]);
ctx.lineTo(point1[0], point1[1]);
//绘制三角形2,顺序:p4--p5--p6--p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); //描边
ctx.fill(); //填充
复制代码
咱们来具体研究一下fill
函数,查看MDN上的解释,
The
CanvasRenderingContext2D
.fill() method of the Canvas 2D API fills the current or given path with the current fill style using the non-zero or even-odd winding rulevoid ctx.fill([fillRule]); void ctx.fill(path[, fillRule]); 复制代码
fillRule参数是可选的,可取值为nonzero
,evenodd
。也就是说,fill
函数能够给当前路径或者给定的路径,使用非零环绕规则或者奇偶规规则来填充。path 参数是一个Path2D
对象,是一个给定的路径,canvas中默认的是当前路径,这个参数并非全部的浏览器都支持,目前看,还有IE系列和移动设备上都没有很好的支持,就很少说了,具体能够查看Path2D。
对于路径中的任意给定区域,从该区域内部画出一条足够长的线段,使此线段的终点彻底落在路径范围以外。接下来,将计数器初始化为0,而后,每当这条线段与路径上的直线或者曲线相交时,就改变计数器的值。若是是与路径的顺时针部分相交,则加1,若是是与路径的逆时针部分相交,则减1。最后,如计数器的值不为0,则此区域就在路径里面,调用fill
时,该区域被填充。若是计数器的最终值为0,则此区域就不在路径里面,调用fill
时,该区域就不被填充。canvas的fill
默认使用的就是这种非零环绕规则。
再来看看上图,为什么中间交叉区域没有被填充。咱们绘制了2个三角形,第一绘制顺序是p1-->p2-->p3-->p1,第二个绘制顺序是p4-->p5-->p6-->p4 。能够看到第一个三角形在绘制是逆时针方向的,第二个三角形绘制是顺时针方向的,中间相交区域的计数器最终值就为0了,因此不该该包含在这个路径中。
非零环绕规则演示能够查看个人示例:非零环绕示例
跟非零环绕规则相似,都是从任意区域画出一条足够长的线,使此线段的终点彻底落在路径范围以外。若是这个线段与路径相交的个数位奇数,则此区域包含在路径中,若是为偶数,则表示此区域不包含在路径中。
例如,咱们把上面的例子改下,绘制第二个三角形的顺序改为逆时针p4-->p6-->p5--P4,而后分别用非零环绕规则和奇偶规则来填充,看看效果。
//绘制三角形2,注意顺序变了:p4-p6-p5-p4
ctx.moveTo(point4[0], point4[1]);
ctx.lineTo(point6[0], point6[1]);
ctx.lineTo(point5[0], point5[1]);
ctx.lineTo(point4[0], point4[1]);
ctx.stroke(); //描边
ctx.fill(); //填充, 默认就是非零环绕规则
复制代码
上面两个三角形的顺序都是逆时针,因此按照非零环绕规则,像个三角形的相交区域的计数器的最终值为-2,不为0,则包含在路径中改,被填充了。
一样的顺序,咱们在改用奇偶规则来填充。
ctx.fill('evenodd'); //填充, 改用奇偶规则
复制代码
这篇咱们主要学习了canvas中如何绘制图形,好比常见的三角形,四边形,圆心,以及任意多边形。在绘制图形时,有些好比矩形,圆形等canvas已经提供了内置的函数,ctx.rect()
和ctx.arc
能够直接绘制,可是对于任意多边形,咱们则须要本身逐线段的绘制。
在绘制路径时,是有顺序的。理解canvas中路径,和当前绘制的顺序,就能够很好的理解了canvas中填充规则了。canvas中填充有非零环绕规则和奇偶规则。对于一样的路径,不一样的规则可能会产生不一样的填充区域,是使用时,注意路径顺序就行了。