本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,博客地址为http://www.cnblogs.com/jasonnode/ 。网站上有对应每一小节的在线练习大家可以去试试。
1.路径、描边与填充
迄今为止,在本章之中我们所绘制的唯一图形,就是通过在Canvas的绘图环境对象上调用strokeRect()方法所画的矩形。我们也通过调用fillRect()方法对其进行了填充。这两个方法都是立即生效的。实际上,它们是Canvas绘图环境中仅有的两个可以用来立即绘制图形的方法(strokeText()与fillText()方法也是进行立即绘制的,但文本不算是图形)。绘图环境对象中还有一些方法,用于绘制诸如贝塞尔曲线(bézier curve)这样更为复杂的图形,这些方法都是基于路径(path)的。
大多数绘制系统,例如Scalable Vector Graphics(可缩放向量图形,简称SVG)、Apple的Cocoa框架,以及Adobe Illustrator等,都是基于路径的。使用这些绘制系统时,你需要先定义一个路径,然后再对其进行描边(也就是绘制路径的轮廓线)或填充,也可以在描边的同时进行填充。图2-13演示了这三种绘制方式。
该应用程序创建了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()方法,对刚才那些路径进行描边或填充。
描边与填充操作的效果取决于当前的绘图属性,这些属性包括了lineWidth、strokeStyle、fillStyle以及阴影属性等。比如,程序清单2-9中的这个应用程序,将lineWidth属性值设置为2,然后对文本进行描边,其后又将其重置为5,再对路径进行描边。
由rect()方法所创建的路径是封闭的,然而,arc()方法创建的圆弧路径则不封闭,除非你用它创建的是个圆形路径。要封闭某段路径,必须像程序清单2-9中那样,调用closePath()方法才行。
表2-5总结了本应用程序中与路径相关的方法。
提示:路径与隐形墨水
有一个很恰当的比喻,可以用来说明“创建路径随后对其进行描边或填充”这个操作。我们可以将该操作比作“使用隐形墨水来绘图”。
你用隐形墨水所绘制的内容并不会立刻显示出来,必须进行一些后续操作,像是加热、涂抹化学药品、照射红外线等,才可以将你所画的内容显示出来。如果读者关注这个话题,可以在http://en.wikipedia.org/wiki/Invisible_ink读到所有关于隐形墨水的知识。
使用rect()与arc()这样的方法来创建路径,就好比使用隐形墨水来进行绘制一样。这些方法会创建一条不可见的路径,稍后可以调用stroke()或fill()令其可见。
2. 路径与子路径
在某一时刻,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,所以它们的起点所在的区域就会被浏览器填充。
3. 剪纸效果
我们来运用一下所学到的路径、阴影以及非零环绕原则等知识,实现如图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();
}
}
}