《HTML5游戏编程核心技术与实战》——2.2 图形API-阿里云开发者社区

开发者社区> 异步社区> 正文

《HTML5游戏编程核心技术与实战》——2.2 图形API

简介:
+关注继续查看

本节书摘来自异步社区《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所示。


5cd63781e1f7c16ee5c0efbfe7fa9a8e7e6a8469


0298280ed0a66db8d75aca78618a45900768b69b

**
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所示。


<a href=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)为右下角的矩形区域。这个方法在进行动画处理的时候非常有用,因为在连续绘制动态图形的时候,需要先清除画布上的一块区域。


e2c7f0755e4c6d546624e1207a4cd5bc55857a4d

对于图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>


61e69624ee13da714423659746601c78876adc4c

需要注意的是,代码在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分别显示了三次和二次的贝塞尔曲线,区别在于三次的贝塞尔曲线多了一个控制点。


14e216191bcb75c364fb4c0628c5ebf287d1fc2e

以下代码在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所示。


<a href=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所示。


<a href=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所示。


25d8950e319b6e50c8d9db442c6da655870881a3

除了可以填充纯色以外,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所示。


8fcf7ef34a751957c0e0a8ac5a687b05ca65df85

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关于图像处理的部分。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
如何用 TypeScript 代码区分一个 button 动作是由键盘还是鼠标触发的
如何用 TypeScript 代码区分一个 button 动作是由键盘还是鼠标触发的
7 0
一个用于 Angular 开发的 Chrome 扩展 - Angular Dev Tools
一个用于 Angular 开发的 Chrome 扩展 - Angular Dev Tools
4 0
通过一个简单的例子,了解如何单步调试 Cypress 代码
通过一个简单的例子,了解如何单步调试 Cypress 代码
4 0
Gin 框架:启动多个端口
本文介绍如何通过 rk-boot 在一个进程里启动多个 Gin 端口。
6 0
中英文混版时的间距突然变大怎么通过CSS解决?
中英文混版时的间距突然变大怎么通过CSS解决?
5 0
Chrome 开发者工具 Initiator 面板单击后看不到 JavaScript 源代码的解决方法
Chrome 开发者工具 Initiator 面板单击后看不到 JavaScript 源代码的解决方法
5 0
如何修改Cypress 测试代码中默认的超时时间(timeout)
如何修改Cypress 测试代码中默认的超时时间(timeout)
5 0
Angular 项目中的可摇树依赖 - Tree-shakable dependencies
Angular 项目中的可摇树依赖 - Tree-shakable dependencies
5 0
Chrome Elements 标签页 和 View Source 的显示为什么有差异
Chrome Elements 标签页 和 View Source 的显示为什么有差异
5 0
单页面 Web 应用(Single Page Application,SPA)的工作原理介绍
单页面 Web 应用(Single Page Application,SPA)的工作原理介绍
5 0
+关注
异步社区
异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
11935
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载