本节书摘来自异步社区《HTML5游戏编程核心技术与实战》一书中的第2章,第2.2节,作者: 向峰 更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.2 图形API
创建canvas和获取了canvas的环境上下文之后,就可以开始进行绘图了。绘图的方式有两类:一类是进行图形操作,另一类是图像操作。本小节主要涉及图形相关的API,要使用canvas的API进行绘图,通常需要进行下列步骤。
(1)获取canvas元素。通过document.getElementById()取得元素。
(2)获取canvas元素的环境上下文。通过canvas.getContext ("2d")获取2D图像上下文。
(3)确定绘图模式。使用canvas绘图有两种模式,一种是fill,另外一种是stroke。fill是填充的意思,使用该方式模式进行绘图时候会把颜色填充整个图形,而使用stroke的模式只会进行描边框。
(4)设定绘图样式。通过fillStyle和strokeStyle指定绘图样式,fillStyle和strokeStyle分别对应fill模式和stroke模式。绘图样式包括绘图的颜色及渐变方式,通常情况下默认的绘图样式颜色是#000000,也就是黑色。
(5)指定线宽。可以通过lineWidth设定绘制的线宽,默认值是1.0像素。
在进行绘制图形之前,需要先理解路径的概念。
2.2.1 理解路径
canvas中所有的图形可以看成一条路径,这条路径包含0个或者多个子路径。我们可以把路径看成当前canvas中所有图形的集合,而每一个图形就是一条子路径,一条子路径是由一系列点的集合组成。举个例子,我们在canvas中画了一个圆和一条直线,那么可以认为当前canvas中包含两条子路径,一条是圆,一条是直线,这两个图形都是由一个个点集组成,这个圆称为一个闭合的路径,而线是没有闭合的路径。很明显,所谓闭合就是整个图形是封闭的,图形的开始点和结束点相互连接。
事实上,为了提高绘制的效率,当使用canvas进行绘图的时候,所有的图形操作都只是往当前子路径上填加图形,并不是真正的调用绘图操作。比如使用lineTo()进行画线操作,实际上它只是往当前子路径填加一条直线,最终调用stroke或者fill的时候,才是真正进行硬件操作进行绘图。
2.2.2 路径操作API
canvas中常见的创建和渲染路径的方法如表2-1所示。
**
2.2.3 绘制线条**
关于线条的绘制主要包含以下两个常用的方法。
context.moveTo (x, y):把画笔移动到(x, y)坐标,建立新的子路径。
context.lineTo (x, y):用于建立上一个点到(x, y)坐标的直线,如果没有上一个点,则等同于moveTo (x, y),把(x ,y)点添加到子路径中。
最后使用stroke()可以对路径进行描边。
使用context.lineTo (x, y)绘制直线,代码如下:
<body>
<canvas id="can" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
ctx.moveTo(0, 0);
ctx.lineTo(width, height);
ctx.lineWidth=6;
ctx.strokeStyle = “red”
//开始画线
ctx.stroke();
</script>
代码首先获取can元素的环境上下文,获得can元素的宽度和高度,把画笔移动到原点处,然后使用lintTo (width, height),建立一条从原点到右下角的直线,设定线的宽度为6个像素,笔的颜色是红色,最后通过stroke对整个路径描边。
最后的效果如图2-2所示。
如果,我们需要绘制更复杂的图形,就需要根据一些点的集合,不断的使用lineTo方法,比如下面的代码就绘制了一个向右的箭头:
<body>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
var pts=[[30, 100], [300, 100], [300, 50], [350, 130], [300, 210], [300, 160], [30, 160]]
ctx.strokeStyle="red";
ctx.lineWidth = 2;
ctx.moveTo(pts[0][0], pts[0][1]);
for(var i=1;i<pts.length;i++)
{
ctx.lineTo(pts[i][0], pts[i][1]);
}
ctx.closePath();
ctx.stroke();
</script>
最后的效果如图2-3所示。
https://yqfile.alicdn.com/8f3e3ceba43c269ea30c3ea17fccd10f483b1b78.png" >
这里需要注意的是,这里定义了7个点,在进行stroke之前使用closePath()函数闭合了路径,这时就会把最后一个点和第一个点连接起来,形成一个封闭的图形,当然,closePath并不是必需的。
2.2.4 绘制矩形
关于矩形的绘制主要包含以下两个常用的方法。
rect (x, y, w, h):建立两个子路径,一个是以点(x, y)为左上角,w和h分别为宽度和高度的矩形,另一个是点(x, y)。这个方法只是建立路径,所以当进行绘制的时候还需要使用stroke()方法描边。
最直接的方法是使用strokeRect (x, y, w, h),该方法会以(x, y)为左上角,(x+width, y+height)为右下角绘制矩形。
fillRect (x, y, w, h)方法则以(x, y)为左上角,(x+width, y+height)为右下角填充矩形。
clearRect (x, y, w, h)则用来清除以(x, y)为左上角,(x+width, y+height)为右下角的矩形区域。这个方法在进行动画处理的时候非常有用,因为在连续绘制动态图形的时候,需要先清除画布上的一块区域。
对于图2-4所示的图形,代码如下:
<body>
<h2>画矩形例子</h2>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算最里面矩形左上角坐标,边长为10
var xOff = width*0.5+5, yOff = height*0.5+5;
for(var i=0;i<8;i++)
{
//以最里面矩形为中心,画同心矩形,边长增加20
ctx.strokeRect(xOff-10*i, yOff-10*i, i*20+10, i*20+10);
}
</script>
这段代码使用了strokeRect方法画出了8个同心的矩形。
2.2.5 绘制圆弧
关于圆弧的绘制主要包含以下两个常用的方法。
arc (x, y, radius, startAngle, endAngle, anticlockwise):arc方法用来绘制一段圆弧路径,以(x, y)为圆心位置、radius为半径、startAngle为起始弧度、endAngle为终止弧度来画,而在画圆弧时的旋转方向则由最后一个参数 anticlockwise 来指定,如果为 true 就是逆时针,false则为顺时针,如果startAngle和endAngle分别为0和2*Math.PI,则就变成了绘制圆形。
arcTo (x1, y1, x2, y2, radius):这个函数实际上用来绘制同时和两条直线相切的,半径为radius的最短圆弧,一条直线以上一个点和(x1, y1)构成,另一条直线以(x1, y1)和(x2, y2)构成。
这两个函数只是把圆弧添加到了路径中,如果绘制,则还需要通过stroke或者fill函数。
使用arc方法绘制图2-5所示的8个同心圆的代码如下:
<body>
<h2>画圆形例子</h2>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算圆心
var xOff = width*0.5,
yOff = height*0.5;
for(var i=1;i<8;i++)
{
//以最里面矩形为中心,画同心圆,半径依次增加15
ctx.beginPath();
ctx.arc(xOff, yOff, i*15, 0, Math.PI*2, true);
ctx.closePath();
ctx.stroke();
}
</script>
需要注意的是,代码在for循环中,进行绘制圆形之前,使用了beginPath()方法,beginPath()方法用于清除掉之前的路径。如果不清除的话,那么,每次绘制的时候都会把之前的路径又绘制,这样会降低绘制的效率。所以通常情况下,如果我们决定要绘制一个新的图形,最好先使用beginPath()清除上一次的路径。
2.2.6 绘制贝塞尔曲线
关于贝塞尔曲线的绘制主要包含以下两个常用的方法。
bezierCurveTo (cp1x, cp1y, cp2x, cp2y, x, y):绘制一条三次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cp1x, cp1y)和(cp2x, cp2y)。
quadraticCurveTo (cpx, cpy, x, y):绘制一条二次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cpx, cpy)。
贝塞尔曲线是应用非常广泛的函数曲线,通常在计算机图形中用来为平滑曲线建立模型,图2-6分别显示了三次和二次的贝塞尔曲线,区别在于三次的贝塞尔曲线多了一个控制点。
以下代码在canvas中显示了一个可以调节控制点的贝塞尔曲线,c1和c2表示控制点,s和e表示曲线的开始和终止点:
<!DOCTYPE html>
<meta charset="utf-8" />
<style type="text/css">
body{text-align:center;}
#can{border:1px solid black}
</style>
<body>
<h2>贝塞尔曲线</h2>
<canvas id="can" width="400" height="300"></canvas>
</body>
<script>
var ctx = can.getContext("2d");
//定义Point对象
var Point = function(x, y){
this.x = x;
this.y = y;
}
//定义控制点,前面两个是开始和结束点,最后两个是控制点
var cPt =[];
//产生控制点
function createControlPt(x, y)
{
if(cPt.length<4)
{
cPt.push(new Point(x, y));
}
}
//绘制控制点
function drawPt()
{
for(var i=0;i<cPt.length;i++)
{
var c = "red";
if(i<2)
{
c = "green";
}
ctx.strokeStyle = c;
ctx.strokeRect(cPt[i].x-5, cPt[i].y-5, 10, 10);
}
}
//判断一个点是否在一个以p2为中心的矩形中
function isInRect(p1, p2, w, h)
{
return p1.x>=p2.x-w&&p1.x<=p2.x+w&&p1.y>=p2.y-h&&p1.y<=p2.y+h;
}
//判断一个点在哪一个控制区域中
function getIdxCpt(p)
{
var idx = -1;
for(var i=0;i<cPt.length;i++)
{
if(isInRect(p, cPt[i], 5, 5))
{
return i;
}
}
return idx;
}
//绘制控制点和起始点连线
function drawBLine()
{
ctx.strokeStyle = "gray";
ctx.beginPath();
ctx.moveTo(cPt[0].x, cPt[0].y);
ctx.lineTo(cPt[2].x, cPt[2].y);
ctx.stroke();
ctx.moveTo(cPt[1].x, cPt[1].y);
ctx.lineTo(cPt[3].x, cPt[3].y);
ctx.stroke();
}
//绘制贝塞尔曲线
function drawBei()
{
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.moveTo(cPt[0].x, cPt[0].y);
ctx.bezierCurveTo(cPt[2].x, cPt[2].y, cPt[3].x, cPt[3].y, cPt[1].x, cPt[1].y);
ctx.stroke();
}
//绘制所有的图形
function draw()
{
drawPt();
if(cPt.length>3)
{
drawBLine();
drawBei();
}
}
//设置鼠标点下和移动事件
var selPt = new Point(-1, -1), sIdx;
can.onmousedown = function(e){
var x = e.offsetX, y = e.offsetY;
selPt.x = x;selPt.y = y;
createControlPt(x, y);
draw();
//判断是否点在控制点中
if(cPt.length>3)
{
sIdx = getIdxCpt(selPt);
if(sIdx>=0)
{
can.onmousemove = function(e){
cPt[sIdx].x = e.offsetX;
cPt[sIdx].y = e.offsetY;
ctx.clearRect(0, 0, 400, 300);
draw();
}
}
}
}
can.onmouseup = function(){
this.onmousemove = null;
}
</script>
</html>
注意在绘制所有图形之前一定要使用clearRect()方法来清除屏幕,因为如果不清除屏幕,将会在屏幕上留下所有的绘制图像。
2.2.7 线条属性
在进行图形绘制的时候,线条有一些常用的属性会影响到线条的样式。
lineWidth:该属性用来设置线条的粗细,默认为1个像素大小,小于0的值将被忽略。
这里有一个比较经典的问题,就是绘制1像素大小的直线。如果绘制1像素大小的线条,看起来像2个像素,当直线呈水平或者垂直方向时,这个现象非常明显,这是为什么呢?W3C在canvas规范中解释到,当使用canvas绘制图形时候,它是由路径向两边扩展的,各占绘制线条宽度的一半,但canvas的坐标并不是直接和屏幕上的像素对应,假设需要绘制一条(3, 1)到(3, 5)的直线,把屏幕放大,得到图2-7。
图2-7中的每一个格子代表显示屏的一个像素,当绘制(3, 1)到(3, 5)的直线的时候,首先,路径就定位在屏幕中第三列像素和第四列像素的中间位置。此时,绘制1像素的时候,就需要从这条路径分别向两边扩展0.5个像素,但实际上显示屏是不可能绘制半个像素的,这个时候就只能同时绘制第三列和第四列两列像素。所以如果需要屏幕绘制一个像素大小的线,只需要把canvas的路径定位到某一个像素的中间位置,这时候刚好向两边扩展为一个像素,如图2-8所示。
https://yqfile.alicdn.com/bd4e6b1536bc727fde07c79a3df2fac7849fa47d.png" >
所以,如果需要绘制1像素大小的直线,需要把坐标加上0.5的偏移,这时候就显示正常了,当然,如果画大于1像素的或者绘制斜线就没有必要额外处理了。
lineCap:lineCap用来指定线条两端的端点,常用的值有3个,分别是butt(无端点)、round(圆端点)以及square(方端点),其中默认值是butt,三种样式显示的效果如图2-9所示。
lineJoin:lingJoin用来设置两条线连接的方式,常用值有round(圆角)、bevel(斜角)以及miter(尖角),其中miter是默认值,三种样式如图2-10所示。
https://yqfile.alicdn.com/9be8a2c38c190e999497bbac404b4452d2fc265f.png" >
miterLimit:当lineJoin为miter时有效,表示的是斜面长度和线宽的比例,默认为10。
2.2.8 线条颜色
线条的颜色使用stokeStyle属性指定,颜色的值可以使用类似CSS的方式指定,比如红色可以采用以下3种方式:
- context.stokeStyle = ‘red‘
- context.stokeStyle = ‘#ff0000’
- context.stokeStyle = ‘rgba(255, 0, 0, 1.0)’
2.2.9 填充
前面所介绍的绘图方式都是适用描边处理(stroke),我们可以通过fill方法进行图形填充。
fill():该方法使用当前的fillStyle填充当前路径,通过fillStyle = 颜色值可以指定填充的颜色,颜色表示和strokeStyle一样。
以下代码就填充了8个红色的同心圆:
<body>
<h2>填充圆形例子</h2>
<canvas id="can" width="400" height="300" >
</canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算圆心
var xOff = width*0.5,
yOff = height*0.5;
for(var i=1;i<=8;i++)
{
//以最里面矩形为中心,画同心圆,半径依次减少15
ctx.beginPath();
ctx.fillStyle = "rgba(255, 0, 0, "+(30*i)/500+")";
ctx.arc(xOff, yOff, 120-i*15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
</script>
最后的效果如图2-11所示。
除了可以填充纯色以外,canvas还提供了填充渐变色以及填充贴图,先来看看渐变对象。
经常使用Photoshop处理图像的读者知道,在Photoshop中就有这种渐变工具,可以通过拖拉一条辅助线来实现渐变。在canvas中提供的渐变对象有两种,一种是线性渐变,另一种是径向渐变。
createLinearGradient (x0, y0, x1, y1):创建一个线性的渐变对象,开始点是(x0, y0),结束点是(x1, y1)。
createRadialGradient (x0, y0, r0, x1, y1, r1):创建一个径向渐变对象,开始点以(x0, y0)为圆心,r0为半径,结束点以(x1, y1)为圆心,r1为半径。
一旦创建完了渐变对象之后,就可以通过该对象的addColorStop()方法,在渐变的某一点中增加一个颜色值,这个点可以认为是关键点,这样,每个关键点之间的色彩就会出现渐变效果。
addColorStop(offset, color):offset表示偏移大小,值在0.0~1.0之间,其实就是一个百分比值;color是使用类似CSS字符串描述的色彩颜色,比如addColorStop (0, "red")表示初始关键点是一个红色点。
当使用fillStyle属性指定一个渐变对象的时候,就可以使用渐变的方式填充路径了,以下代码以渐变对象填充了两个矩形区域。
<body>
<h2>渐变</h2>
<canvas id="can" width=600 height=300></canvas>
</body>
<script>
var ctx = can.getContext("2d");
var w = 480, h=60;
ctx.beginPath();
//创建线性渐变
var g = ctx.createLinearGradient(0, 0, 480, 0);
//创建径向渐变
var g1 = ctx.createRadialGradient(300, 160, 10, 300, 160, 240);
//设置颜色
g.addColorStop(0, "black");
g.addColorStop(1, "white");
ctx.fillStyle = g;
//绘制矩形
ctx.rect((600-w)*0.5, 30, w, 80);
ctx.fill();
ctx.beginPath();
//定义基本色
var colors=["aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon",
"navy", "olive", "purple", "red", "silver", "teal", "white", "yellow"];
var step = 1/colors.length;
for(var i=0;i<colors.length;i++)
{
g1.addColorStop(i*step, colors[i]);
}
//ctx.arc(300, 200, 100, 0, Math.PI*2, true);
//绘制矩形
ctx.rect((600-w)*0.5, 120, w, 80);
ctx.fillStyle = g1;
ctx.fill();
</script>
效果如图2-12所示。
以上是使用渐变颜色进行填充,另外一种填充方式是使用一张图片作为贴图进行填充,使用的API如下。
createPattern (image,repetition):image表示需要填充的图像,可以是img、canvas、video元素等;repetition定义图像按照什么方式贴图,通常的贴图方式有以下几种。
- repeat:水平和垂直方向重复贴图,默认值。
- repeat-x:水平方向重复贴图。
- repeat-y:垂直方向重复贴图。
- no-repeat:使用一次贴图。
通过createPattern方法创建了一个模式对象后,就可以通过fillStyle或者strokeStyle等属性指定,然后就可以使用指定的图形进行填充。
以下代码创建了两个分别使用stroke()和fill()填充的图形:
<body>
<h2>Pattern</h2>
<canvas id="can" width="600" height="300"></canvas>
</body>
<script>
var ctx = can.getContext('2d');
var imgSrc =["img/t1.png", "img/f1.png"];
var ctx = can.getContext("2d");
//创建Image对象和pattern对象
for(var i=0;i<imgSrc.length;i++)
{
var img = new Image();
img.src = imgSrc[i];
img.onload = (function(im, i){
var self = im;
return function(){
var p = ctx.createPattern(self, "repeat");
if(i==0)
{
ctx.beginPath();
ctx.fillStyle = p;
ctx.fillRect(38, 38, 520, 232);
}
else
{
ctx.beginPath();
ctx.strokeStyle = p;
ctx.lineWidth = 18;
ctx.strokeRect(28, 28, 540, 250);
}
}
}(img, i));
}
</script>
效果如图2-13所示。
2.2.10 绘图状态
conext中有一些全局的属性,如前面提到的strokeStyle、fillStyle、lineWidth等。当我们进行绘图的时候,有时,在改变这些值之前,需要保存上一次绘图的状态,下次绘制的时候又需要进行恢复,这种情况很常见。当然,不需要我们自己定义一个全局的对象进行保存,context中本身定义了以下方法用于保存和恢复canvas的状态。
save():把当前绘图状态压到绘图状态堆中。
restore():弹出绘图状态堆最上面保存的绘图状态。
状态堆中包含以下部分。
当前的transformation matrix(换矩阵)前的clipping region(区域)。
当前的属性值:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。
为了避免本次绘图状态影响到下次绘图,通常情况下在绘图之前,都会使用contex.save()方法保存当前绘图状态,绘制完成后再使用context.restore()进行恢复。
对于图形的操作,在HTML4时代可以使用SVG进行矢量绘图,而canvas除了支持矢量图形外,还可以直接针对图像以及像素操作,这才是canvas强大的地方。接下来,看看canvas关于图像处理的部分。