迄今为止,在本章之中咱们所绘制的惟一图形,就是经过在Canvas的绘图环境对象上调用strokeRect()方法所画的矩形。咱们也经过调用fillRect()方法对其进行了填充。这两个方法都是当即生效的。实际上,它们是Canvas绘图环境中仅有的两个能够用来当即绘制图形的方法(strokeText()与fillText()方法也是进行当即绘制的,但文本不算是图形)。绘图环境对象中还有一些方法,用于绘制诸如贝塞尔曲线(bézier curve)这样更为复杂的图形,这些方法都是基于路径(path)的。node
大多数绘制系统,例如Scalable Vector Graphics(可缩放向量图形,简称SVG)、Apple的Cocoa框架,以及Adobe Illustrator等,都是基于路径的。使用这些绘制系统时,你须要先定义一个路径,而后再对其进行描边(也就是绘制路径的轮廓线)或填充,也能够在描边的同时进行填充。图2-13演示了这三种绘制方式。canvas
该应用程序建立了9个不一样的路径,对左边一列的路径进行了描边操做,对中间一列的路径进行了填充,并对右边一列的路径同时进行描边与填充。浏览器
第一行的矩形路径与最后一行的圆弧路径都是封闭路径(closed path),而中间一行的弧形路径则是开放路径(open path)。请注意,不论一个路径是开放或是封闭,你均可以对其进行填充。当填充某个开放路径时,浏览器会把它当成封闭路径来填充。图中右边一列的中间那个图形,就是这种效果。框架
程序清单2-9列出了图2-13中那个应用程序的代码。函数
程序清单2-9 文本、矩形与圆弧的描边及填充网站
var context=document.getElementById('drawingCanvas').getContext('2d'); //Functions... function drawGrid(context,color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. } //Initialization... drawGrid(context,'lightgray',10,10); //Drawing attributes... context.font='48pt Helvetica'; context.strokeStyle='blue'; context.fillStyle='red'; context.lineWidth='2'; //Line width set to 2 for text //Text... context.strokeText('Stroke',60,110); context.fillText('Fill',440,110); context.strokeText('Stroke&Fill',650,110); context.fillText('Stroke&Fill',650,110); //Rectangles... context.lineWidth='5'; //Line width set to 5 for shapes context.beginPath(); context.rect(80,150,150,100); context.stroke(); context.beginPath(); context.rect(400,150,150,100); context.fill(); context.beginPath(); context.rect(750,150,150,100); context.stroke(); context.fill(); //Open arcs... context.beginPath(); context.arc(150,370,60,0,Math.PI*3/2); context.stroke(); context.beginPath(); context.arc(475,370,60,0,Math.PI*3/2); context.fill(); context.beginPath(); context.arc(820,370,60,0,Math.PI*3/2); context.stroke(); context.fill(); //Closed arcs... context.beginPath(); context.arc(150,550,60,0,Math.PI*3/2); context.closePath(); context.stroke(); context.beginPath(); context.arc(475,550,60,0,Math.PI*3/2); context.closePath(); context.fill(); context.beginPath(); context.arc(820,550,60,0,Math.PI*3/2); context.closePath(); context.stroke(); context.fill();
首先调用beginPath()方法来开始一段新的路径,rect()与arc()方法分别用于建立矩形及弧形路径。而后,应用程序在绘图环境对象上调用stroke()与fill()方法,对刚才那些路径进行描边或填充。spa
描边与填充操做的效果取决于当前的绘图属性,这些属性包括了lineWidth、strokeStyle、fillStyle以及阴影属性等。好比,程序清单2-9中的这个应用程序,将lineWidth属性值设置为2,而后对文本进行描边,其后又将其重置为5,再对路径进行描边。rest
由rect()方法所建立的路径是封闭的,然而,arc()方法建立的圆弧路径则不封闭,除非你用它建立的是个圆形路径。要封闭某段路径,必须像程序清单2-9中那样,调用closePath()方法才行。code
表2-5总结了本应用程序中与路径相关的方法。对象
提示:路径与隐形墨水
有一个很恰当的比喻,能够用来讲明“建立路径随后对其进行描边或填充”这个操做。咱们能够将该操做比做“使用隐形墨水来绘图”。
你用隐形墨水所绘制的内容并不会马上显示出来,必须进行一些后续操做,像是加热、涂抹化学药品、照射红外线等,才能够将你所画的内容显示出来。若是读者关注这个话题,能够在http://en.wikipedia.org/wiki/Invisible_ink读到全部关于隐形墨水的知识。
使用rect()与arc()这样的方法来建立路径,就比如使用隐形墨水来进行绘制同样。这些方法会建立一条不可见的路径,稍后能够调用stroke()或fill()令其可见。
在某一时刻,canvas之中只能有一条路径存在,Canvas规范将其称为“当前路径”(current path)。然而,这条路径却能够包含许多子路径(subpath)。而子路径,又是由两个或更多的点组成的。比方说,能够像这样绘制出两个矩形来:
context.beginPath(); //Clear all subpaths from //the current path context.rect(10,10,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four points context.beginPath(); //Clear all subpaths from the //current path context.rect(50,50,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four point
以上这段代码经过调用beginPath()来开始一段新的路径,该方法会将当前路径中的全部子路径都清除掉。而后,这段代码调用了rect()方法,此方法向当前路径中增长了一个含有4个点的子路径。最后,调用stroke()方法,将当前路径的轮廓线描绘出来,使得这个矩形出如今canvas之中。
接下来,这段代码又一次调用了beginPath()方法,该方法清除了上一次调用rect()方法时所建立的子路径。而后,再一次调用rect()方法,此次仍是会向当前路径中增长一段含有4个点的子路径。最后,对该路径进行描边,使得第二个矩形也出如今了canvas之中。
如今考虑一下,若是将第二个beginPath()调用去掉,会怎么样呢?像是这样:
context.beginPath(); //Clear all subpaths from the //current path context.rect(10,10,100,100);//Add a subpath with four points context.stroke(); //Stroke the subpath containing //four points context.rect(50,50,100,100);//Add a second subpath with //four points context.stroke(); //Stroke both subpaths
上面这段代码在一开始与刚才那段是同样的:先调用beginPath()来清除当前路径中的全部子路径,而后调用rect()来建立一条包含矩形4个点的子路径,再调用stroke()方法使得这个矩形出如今canvas之上。
接下来,这段代码再次调用了rect()方法,不过这一次,因为没有调用beginPath()方法来清除原有的子路径,因此第二次对rect()方法的调用,会向当前路径中增长一条子路径。最后,该段代码再一次调用stroke()方法,此次对stroke()方法的调用,将会使得当前路径中的两条子路径都被描边,这意味着它会重绘第一个矩形。
填充路径时所使用的“非零环绕规则”
若是当前路径是循环的,或是包含多个相交的子路径,那么Canvas的绘图环境变量就必需要判断,当fill()方法被调用时,应该如何对当前路径进行填充。Canvas在填充那种互相有交叉的路径时,使用“非零环绕规则”(nonzero winding rule)来进行判断。图2-14演示了该规则的运用。
“非零环绕规则”是这么来判断有自我交叉状况的路径的:对于路径中的任意给定区域,从该区域内部画一条足够长的线段,使此线段的终点彻底落在路径范围以外。图2-14中的那三个箭头所描述的就是上面这个步骤。
接下来,将计数器初始化为0,而后,每当这条线段与路径上的直线或曲线相交时,就改变计数器的值。若是是与路径的顺时针部分相交,则加1,若是是与路径的逆时针部分相交,则减1。若计数器的最终值不是0,那么此区域就在路径里面,在调用fill()方法时,浏览器就会对其进行填充。若是最终值是0,那么此区域就不在路径内部,浏览器也就不会对其进行填充了。
能够从图2-14中看出“非零环绕规则”是如何运用的。左边的那个带箭头的线段,先穿过了路径的逆时针部分,而后又穿过了路径的顺时针部分。这意味着其计数值是0,因此该线段起点所在的那个区域就不在范围内,在调用fill()方法时,浏览器也就不会对其进行填充。然而,其他两条带箭头的线段,其计数值都不是0,因此它们的起点所在的区域就会被浏览器填充。
咱们来运用一下所学到的路径、阴影以及非零环绕原则等知识,实现如图2-15所示的剪纸(cutout)效果。
图2-15所示的应用程序,其JavaScript代码列在了程序清单2-10之中。
这段JavaScript代码建立了一条路径,它由两个圆形所组成,其中一个圆形在另外一个的内部。经过设定arc()方法的最后一个参数值,该应用程序以顺时针方向绘制了内部的圆形,而且以逆时针方向绘制了外围的圆形。绘制效果如图2-16上方所示。
在建立好路径以后,图2-15中的那个应用程序对该路径进行了填充。浏览器运用“非零环绕规则”,对外围圆形的内部进行了填充,不过填充的范围并不包括里面的圆,这就产生了一种剪纸图案的效果。你也能够利用此技术来剪出任意想要的形状来。
程序清单2-10 图2-15所示应用程序的JavaScript代码
var context=document.getElementById('canvas').getContext('2d'); //Functions... function drawGrid(color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. function drawTwoArcs(){ context.beginPath(); context.arc(300,190,150,0,Math.PI*2,false);//Outer:CCW context.arc(300,190,100,0,Math.PI*2,true);//Inner:CW context.fill(); context.shadowColor=undefined; context.shadowOffsetX=0; context.shadowOffsetY=0; context.stroke(); function draw(){ context.clearRect(0,0,context.canvas.width, context.canvas.height); } } drawGrid('lightgray',10,10); context.save(); context.shadowColor='rgba(0,0,0,0.8)'; context.shadowOffsetX=12; context.shadowOffsetY=12; context.shadowBlur=15; drawTwoArcs(); context.restore(); } //Initialization... context.fillStyle='rgba(100,140,230,0.5)'; context.strokeStyle=context.fillStyle; draw();
图2-16之中的例子是对图2-15所示应用程序的一种扩展,它会告诉你若是两个圆形子路径都在同一个方向上,绘制效果会如何,同时它也增长了一些注释信息,用以显示圆形子路径的绘制方向以及“非零环绕规则”的计算过程。并且,这个程序还显示了建立圆形子路径所调用的arc()方法。
提示:图2-16之中的那条横线是怎么回事
请注意图2-16中两个圆之间的那条横线。在图2-15之中也有这样一条线,不过图2-16使用了更深一些的描边颜色,把它画得更加明显了。
根据Canvas规范,当使用arc()方法向当前路径中增长子路径时,该方法必须将上一条子路径的终点与所画圆弧的起点相连。
制做剪纸图形
图2-17之中的应用程序在矩形内剪出了三个图形。与上一小节中所讨论的那个程序不一样,图2-17所示的应用程序采用了彻底不透明的颜色来填充这个包含剪纸图形的矩形。
该应用程序有两个值得注意的地方。首先,包围剪纸图形的是一个矩形而不是圆形。这个矩形的使用向你代表,能够用任意形状的路径来包围剪纸图形,并不必定非要用圆形。该程序创建剪纸图形所用的代码以下:
function drawCutouts(){ context.beginPath(); addOuterRectanglePath();//Clockwise(CW) addCirclePath(); //Counter-clockwise(CCW) addRectanglePath(); //CCW addTrianglePath(); //CCW context.fill();//Cut out shapes }
addOuterRectanglePath()、addCirclePath()、addRectanglePath()及addTrianglePath()方法分别向当前路径中添加了表示剪纸图形的子路径。
图2-17中的应用程序还有一个有意思的地方,就是其中的矩形剪纸图案。arc()方法可让调用者控制圆弧的绘制方向,然而rect()方法则没有那么方便,它老是按照顺时针方向来建立路径。但是,在本例这种状况下,须要的是一条逆时针的矩形路径,因此咱们本身建立了一个rect()方法,此方法像arc()同样,可让调用者控制矩形路径的方向:
function rect(x,y,w,h,direction){ if(direction){//CCW context.moveTo(x,y); context.lineTo(x,y+h); context.lineTo(x+w,y+h); context.lineTo(x+w,y); } else{ context.moveTo(x,y); context.lineTo(x+w,y); context.lineTo(x+w,y+h); context.lineTo(x,y+h); } context.closePath(); }
上述代码使用moveTo()与lineTo()方法来建立顺时针或者逆时针的矩形路径。在2.8节中咱们将详细讲述这些方法。
该应用程序在创建路径时,分别使用了两种不一样的方式来建立外围矩形及内部的矩形剪纸图形:
function addOuterRectanglePath(){ context.rect(110,25,370,335); } function addRectanglePath(){ rect(310,55,70,35,true); }
addOuterRectanglePath()方法使用了绘图环境对象的rect()方法,此方法老是按照顺时针方向来绘制矩形的,并无提供逆时针绘制的选项。addRectanglePath()方法建立了矩形剪纸图形的路径,它使用上面列出的那个rect()方法来绘制逆时针的矩形路径。
图2-17所示应用程序的JavaScript代码列在了程序清单2-11之中。
小技巧:路径方向真的很重要
arc()方法的最后一个boolean参数用于控制所绘圆弧路径的方向。若是该参数是默认值true,那么浏览器就会以顺时针方向来绘制路径,不然,浏览器就按照逆时针(counterclockwise)方向来绘制(或者按照Canvas规范中的说法,“反时针方向”(anti-clockwise))。
提示:arc()方法能够控制路径方向,而rect()方法则不行
arc()方法与rect()方法均可以向当前路径中添加子路径,然而arc()方法可让调用者来控制路径的绘制方向。幸亏能够很是容易地实现一个函数,用它来建立具备特定方向的矩形路径。程序清单2-11中的rect()方法就演示了这种作法。
小技巧:去掉由arc()方法所产生的那条不太美观的链接线
若是在当前路径中存在子路径的状况下调用arc()方法,那么此方法就会从子路径的终点向圆弧的起点画一条线。一般状况下,你并不想看到这条线段。
若是不想让这条连线出现,能够在调用arc()方法来绘制圆弧以前,先调用beginPath()方法。调用此方法会将当前路径下的全部子路径都清除掉,这样一来,arc()方法也就不会再绘制那条链接线了。
程序清单2-11 绘制剪纸图形的代码
var context=document.getElementById('canvas').getContext('2d'); //Functions... function drawGrid(color,stepx,stepy){ //Listing omitted for brevity.See Example 2.13 //for a complete listing. } function draw(){ context.clearRect(0,0,context.canvas.width, context.canvas.height); drawGrid('lightgray',10,10); context.save(); context.shadowColor='rgba(200,200,0,0.5)'; context.shadowOffsetX=12; context.shadowOffsetY=12; context.shadowBlur=15; drawCutouts(); strokeCutoutShapes(); context.restore(); } function drawCutouts(){ context.beginPath(); addOuterRectanglePath();//CW addCirclePath(); //CCW addRectanglePath();//CCW addTrianglePath();//CCW context.fill();//Cut out shapes } function strokeCutoutShapes(){ context.save(); context.strokeStyle='rgba(0,0,0,0.7)'; context.beginPath(); addOuterRectanglePath();//CW context.stroke(); context.beginPath(); addCirclePath(); addRectanglePath(); addTrianglePath(); context.stroke(); context.restore(); function rect(x,y,w,h,direction){ if(direction){//CCW context.moveTo(x,y); context.lineTo(x,y+h); context.lineTo(x+w,y+h); context.lineTo(x+w,y); context.closePath(); } else{ context.moveTo(x,y); context.lineTo(x+w,y); context.lineTo(x+w,y+h); context.lineTo(x,y+h); context.closePath(); } function addOuterRectanglePath(){ context.rect(110,25,370,335); } function addCirclePath(){ context.arc(300,300,40,0,Math.PI*2,true); } function addRectanglePath(){ rect(310,55,70,35,true); } function addTrianglePath(){ context.moveTo(400,200); context.lineTo(250,115); context.lineTo(200,200); context.closePath(); //Initialization... context.fillStyle='goldenrod'; draw(); } } }