canvas核心技术-如何绘制图形

这篇学习和回顾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=0endAngle=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 rule

void 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中填充有非零环绕规则奇偶规则。对于一样的路径,不一样的规则可能会产生不一样的填充区域,是使用时,注意路径顺序就行了。

相关文章
相关标签/搜索